// Copyright (c) Microsoft Corporation.
// 
// Author: Chris King
// Extensively Modified: Vance Morrison
// Date: 5/2007

// see code:#Overview to get started. 
using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using System.Runtime.Serialization;
using EnvDTE;
using EnvDTE80;
using Microsoft.Win32;
using Microsoft.VisualStudio;
using Microsoft.VisualStudio.Shell.Interop;
using Microsoft.VisualStudio.TextManager.Interop;
using Microsoft.VisualStudio.OLE.Interop;
using Microsoft.VisualStudio.Package;
using IOleServiceProvider = Microsoft.VisualStudio.OLE.Interop.IServiceProvider;

/// <summary>
/// #Overview
/// Provides a interface for intercepting Hyperlinks inside Visual Studio. 
/// 
/// Typical use is ot create a HyperLink object (currently can only be one) and call the register a callback
/// using the code:HyperLink.OnHyperLinkClick event. If desired, you can have custom URL prefixes (in
/// addition to the standard ones http:, file:, ..., but calling code:HyperLink.RegisterHyperLinkPrefix
/// 
/// A critical point is where the event is dispatched code:#EventDispatch 
/// </summary>
public sealed class HyperLink
{
    /// <summary>
    /// Must be called to begin sending back HyperLink events. 
    /// </summary>
    public HyperLink(DTE2 dte)
    {
        InterceptHyperlinks.Initialize(this, dte);
    }

    /// <summary>
    /// This handler is called when a hyperlink of any kind is clicked.  
    /// </summary>
    public event EventHandler<HyperLinkEventArgs> OnHyperLinkClick;

    /// <summary>
    /// There are some standard hyperlink prefixes (file:, http:, ..., this API allows you register more. Note
    /// that it requires admin privs (it updates the registry).  Returns true if the registry was updated. 
    /// </summary>
    public bool RegisterHyperLinkPrefix(string prefix)
    {
        // TODO failure to open key.  
        // Try not to open the registry for writing if you don't have to.  
        RegistryKey rk = Registry.ClassesRoot.OpenSubKey(c_protocolHandlerRegKey32);
        RegistryKey subKey = rk.OpenSubKey(prefix);
        bool ret = false;
        if (subKey == null)
        {
            rk.Close();
            rk = Registry.ClassesRoot.OpenSubKey(c_protocolHandlerRegKey32, true);
            subKey = rk.CreateSubKey(prefix);
            ret = true;
        }

        if (subKey != null)
            subKey.Close();
        rk.Close();
        return ret;
    }

    #region Private
    private static readonly string c_protocolHandlerRegKey32 = @"PROTOCOLS\Handler";

    //  #DocLinkTOC
    //  Registration
    //      code:#RegisterForTextManagerEvents Register for IVsTextMangerEvents (document opened) events
    //      code:#RegisterDocumentsOpenedAtStartup Register documents opened before subscribing to doc opened
    //      events code:#UnregisterFromTextManager Unregister IVsTextMangerEvents
    //
    //  Unregistration
    //      code:#AddCommandFilter Register the opened IVsTextView for hyptertext click events
    //      code:#RemoveCommandFilter Register the opened IVsTextView for hyptertext click events
    //
    //  OnClick
    //      code:#HyperLinkEvent 
    //
    //  Bugs
    //      code:#BugDuringRegistrationOfViewsOpenedAtStartup
    //          While enumerating the open documents we try to get a RCW for a pUnk which doesn't want to be
    //          marshaled: 'Microsoft.VisualStudio.Editors.PropPageDesigner.PropPageDesignerDocData' in
    //          Assembly 'Microsoft.VisualStudio.Editors'
    //          
    private class InterceptHyperlinks : IVsTextManagerEvents
    {
        public static void Initialize(HyperLink hyperLink, DTE2 dte)
        {
            if (s_hyperLink != null)
                throw new ApplicationException("HyperLink class must be a singleton");

            s_hyperLink = hyperLink;
            s_dte = dte;
            IVsTextManager textManager = Services.GetIVsTextManager(dte);

            s_httpCommandTarget = new InterceptHyperlinks();
            s_linkTargets = new Dictionary<IVsTextView, HyperlinkCommandTarget>();

            // #RegisterForTextManagerEvents
            s_cookie = new ConnectionPointCookie(textManager, s_httpCommandTarget, typeof(IVsTextManagerEvents));

            #region Register documents opened at start-up
            IVsRunningDocumentTable documentTable = Services.GetRunningDocumentTable(dte);
            IEnumRunningDocuments runningDocuments;
            documentTable.GetRunningDocumentsEnum(out runningDocuments);
            for (; ; )
            {
                uint count = 1, fetched;
                uint[] results = new uint[count];
                runningDocuments.Next(count, results, out fetched);
                if (fetched == 0)
                    break;

                uint rdtFlags, readLocks, editLocks, itemId;
                string documentTableName;
                IVsHierarchy hierarchy;
                IntPtr pUnkDocData;
                documentTable.GetDocumentInfo(results[0],
                    out rdtFlags, out readLocks, out editLocks, out documentTableName,
                    out hierarchy, out itemId, out pUnkDocData);

                object obj = null;
                try { obj = Marshal.GetObjectForIUnknown(pUnkDocData); }
                catch (SerializationException)
                {
                    // #BugDuringRegistrationOfViewsOpenedAtStartup
                    continue;
                }

                IVsTextBuffer textBuffer = obj as IVsTextBuffer;
                if (textBuffer != null)
                {
                    IVsEnumTextViews views;
                    Services.GetIVsTextManager(dte).EnumViews(textBuffer, out views);

                    uint celt = 1;
                    IVsTextView[] rgelt = new IVsTextView[celt];
                    uint celtFetched = 1;
                    views.Next(celt, rgelt, ref celtFetched);

                    if (rgelt[0] != null)
                        // #RegisterDocumentsOpenedAtStartup
                        s_httpCommandTarget.OnRegisterView(rgelt[0]);
                }
            }
            #endregion

            InitializeAppDomainUnloadEvent();
        }
        private static void InitializeAppDomainUnloadEvent()
        {
            AppDomain.CurrentDomain.DomainUnload +=
                delegate(object sender, EventArgs e)
                {
                    // #UnregisterFromTextManager
                    s_cookie.Dispose();

                    foreach (HyperlinkCommandTarget target in s_linkTargets.Values)
                        target.Dispose();
                };
        }

        public class HyperlinkCommandTarget : IOleCommandTarget, IVsIntellisenseHost, IDisposable
        {
            public HyperlinkCommandTarget(IVsTextView textView)
            {
                m_textView = textView;
                m_serviceProvider = null;// = serviceProvider;
                // #AddCommandFilter
                m_textView.AddCommandFilter(this, out m_nextCommandTarget);
            }

            #region IVsIntellisenseHost Members
            public int QueryStatus(ref Guid pguidCmdGroup, uint cCmds, OLECMD[] prgCmds, OLECMDTEXT[] pCmdText)
            {
                throw new Exception("The method or operation is not implemented.");
            }
            public int Exec(ref Guid pguidCmdGroup, uint nCmdID, uint nCmdexecopt, ref object pvaIn, ref object pvaOut)
            {
                throw new Exception("The method or operation is not implemented.");
            }
            public int GetHostFlags(out uint pdwFlags)
            {
                // TODO: determine correct flags
                pdwFlags = (uint)(IntellisenseHostFlags.IHF_NOSEPARATESUBJECT | IntellisenseHostFlags.IHF_FORCECOMMITTOCONTEXT);
                return VSConstants.S_OK;
            }
            public int GetContextBuffer(out IVsTextLines ppCtxBuffer)
            {
                return m_textView.GetBuffer(out ppCtxBuffer);
            }
            public int GetContextFocalPoint(TextSpan[] pSpan, ref int piLen)
            {
                int hr = VSConstants.S_OK;
                piLen = 0;

                //// calculates the extent of the to-be-completed word
                //int caretLine;
                //int caretColumn;
                //hr = m_textView.GetCaretPos(out caretLine, out caretColumn);
                //if (VSConstants.Succeeded(hr))
                //{
                //    hr = m_textView.GetWordExtent(caretLine, caretColumn, 0, span);
                //    piLen = span[0].iEndIndex - span[0].iStartIndex;
                //}

                return hr;

            }
            public int SetContextCaretPos(int iLine, int iIndex)
            {
                return m_textView.SetCaretPos(iLine, iIndex);
            }
            public int GetContextCaretPos(out int piLine, out int piIndex)
            {
                return m_textView.GetCaretPos(out piLine, out piIndex);
            }
            public int SetContextSelection(int iStartLine, int iStartIndex, int iEndLine, int iEndIndex)
            {
                return m_textView.SetSelection(iStartLine, iStartIndex, iEndLine, iEndIndex);
            }
            public int GetContextSelection(TextSpan[] pSelectionSpan)
            {
                return m_textView.GetSelectionSpan(pSelectionSpan);
            }
            public int GetSubjectText(out string pbstrSubjectText)
            {
                pbstrSubjectText = null;
                return VSConstants.E_NOTIMPL;
            }
            public int SetSubjectCaretPos(int iIndex)
            {
                return VSConstants.E_NOTIMPL;
            }
            public int GetSubjectCaretPos(out int piIndex)
            {
                piIndex = 0;
                return VSConstants.E_NOTIMPL;
            }
            public int SetSubjectSelection(int iAnchorIndex, int iEndIndex)
            {
                return VSConstants.E_NOTIMPL;
            }
            public int GetSubjectSelection(out int piAnchorIndex, out int piEndIndex)
            {
                piAnchorIndex = 0;
                piEndIndex = 0;
                return VSConstants.E_NOTIMPL;
            }
            public int ReplaceSubjectTextSpan(int iStartIndex, int iEndIndex, string pszText)
            {
                return VSConstants.E_NOTIMPL;
            }
            public int UpdateCompletionStatus(IVsCompletionSet pCompSet, uint dwFlags)
            {
                throw new Exception("The method or operation is not implemented.");
            }
            public int UpdateTipWindow(IVsTipWindow pTipWindow, uint dwFlags)
            {
                return VSConstants.S_OK;
            }
            public int HighlightMatchingBrace(uint dwFlags, uint cSpans, TextSpan[] rgBaseSpans)
            {
                return VSConstants.S_OK;
            }
            public int BeforeCompletorCommit()
            {
                return VSConstants.S_OK;
            }
            public int AfterCompletorCommit()
            {
                return VSConstants.S_OK;
            }
            public int GetServiceProvider(out Microsoft.VisualStudio.OLE.Interop.IServiceProvider ppSP)
            {
                ppSP = m_serviceProvider as Microsoft.VisualStudio.OLE.Interop.IServiceProvider;
                return VSConstants.S_OK;
            }
            public int GetHostWindow(out IntPtr hwnd)
            {
                hwnd = m_textView.GetWindowHandle();
                return VSConstants.S_OK;
            }
            public int GetContextLocation(int iPos, int iLen, int fUseCaretPosition, RECT[] prc, out int piTopX)
            {
                //// unused
                //prc = new RECT();
                piTopX = 0;
                return VSConstants.S_OK;

            }
            public int UpdateSmartTagWindow(IVsSmartTagTipWindow pSmartTagWnd, uint dwFlags)
            {
                return VSConstants.S_OK;
            }
            public int GetSmartTagRect(RECT[] rcSmartTag)
            {
                return VSConstants.E_NOTIMPL;
            }
            #endregion

            #region IOleCommandTarget Members
            public int QueryStatus(ref Guid pguidCmdGroup, uint cCmds, OLECMD[] prgCmds, IntPtr pCmdText)
            {
                if (pguidCmdGroup == typeof(VSConstants.VSStd2KCmdID).GUID &&
                    cCmds == (uint)VSConstants.VSStd2KCmdID.OPENURL)
                {
                    object o = Marshal.GetObjectForNativeVariant(pCmdText);
                    return VSConstants.S_OK;
                }

                return m_nextCommandTarget.QueryStatus(ref pguidCmdGroup, cCmds, prgCmds, pCmdText); ;
            }
            public unsafe int Exec(ref Guid pguidCmdGroup, uint nCmdID, uint nCmdexecopt, IntPtr pvaIn, IntPtr pvaOut)
            {
                if (pguidCmdGroup != typeof(VSConstants.VSStd2KCmdID).GUID ||
                    nCmdID != (uint)VSConstants.VSStd2KCmdID.OPENURL)
                    return m_nextCommandTarget.Exec(ref pguidCmdGroup, nCmdID, nCmdexecopt, pvaIn, pvaOut);

                // #HyperLinkEvent
                DTE2 dte = s_dte;

                // BUG: The VARIANT does not have its discriminators set so we set it here
                short* vt = (short*)pvaIn.ToPointer();
                *vt = 3;
                int row = (int)Marshal.GetObjectForNativeVariant(new IntPtr(vt));

                // BUG: The VARIANT does not have its discriminators set so we set it here
                vt += 0x08;
                *vt = 3;
                int col = (int)Marshal.GetObjectForNativeVariant(new IntPtr(vt));

                // Line numbers can be counted with or without collapsed text. We map from 
                // collapsed to uncollapsed using LocalLineIndexToBase. 
                IVsLayeredTextView layeredTextView = m_textView as IVsLayeredTextView;
                IVsTextLayer textLayer;
                layeredTextView.GetTopmostLayer(out textLayer);
                int baseRow, baseCol;
                int hr = textLayer.LocalLineIndexToBase(row, col, out baseRow, out baseCol);

                // Set the cursor on the hyperlink so that the hyperlink is pushed onto
                // the stack of previously locations. This way the "previous position" ctrl--
                // command goes back to the hyperlink. I find this useful because I find that
                // I make a "table of content" collection of hyperlinks that I want to goto.
                TextSelection sel = (TextSelection)dte.ActiveDocument.Selection;
                sel.MoveToLineAndOffset(baseRow + 1, baseCol, false);

                //Find 'hyperLinkText' which is the 'word' directly under our cursor
                EditPoint beginLine = sel.ActivePoint.CreateEditPoint();
                beginLine.StartOfLine();
                EditPoint endLine = sel.ActivePoint.CreateEditPoint();
                endLine.EndOfLine();
                string line = beginLine.GetText(endLine);
                int startPos = sel.ActivePoint.LineCharOffset;
                int endPos = startPos;
                while (endPos < line.Length)
                {
                    if (Char.IsWhiteSpace(line[endPos]))
                        break;
                    endPos++;
                }
                while (startPos > 0)
                {
                    if (Char.IsWhiteSpace(line[startPos - 1]))
                        break;
                    --startPos;
                }

                //we require the hyperlink end with a symbolic character (not puntuation). 
                while (endPos > startPos)
                {
                    if (Char.IsLetterOrDigit(line[endPos - 1]))
                        break;
                    --endPos;
                }

                //we require the hyperlink start with a symbolic character (not puntuation). 
                while (startPos < endPos)
                {
                    if (Char.IsLetterOrDigit(line[startPos]))
                        break;
                    startPos++;
                }

                // #EventDispatch
                HyperLinkEventArgs args = new HyperLinkEventArgs();
                args.text = line.Substring(startPos, endPos - startPos);
                if (s_hyperLink.OnHyperLinkClick != null)
                    s_hyperLink.OnHyperLinkClick(dte, args);
                if (!args.Handled)
                    m_nextCommandTarget.Exec(ref pguidCmdGroup, nCmdID, nCmdexecopt, pvaIn, pvaOut);
                return VSConstants.S_OK;
            }
            #endregion

            #region IDisposable Members
            public void Dispose()
            {
                if (m_textView != null)
                    // #RemoveCommandFilter
                    m_textView.RemoveCommandFilter(this);
                m_textView = null;
            }
            #endregion

            private IOleCommandTarget m_nextCommandTarget;
            private IOleServiceProvider m_serviceProvider;
            private IVsTextView m_textView;
        }

        private static InterceptHyperlinks s_httpCommandTarget;
        private static ConnectionPointCookie s_cookie;
        private static Dictionary<IVsTextView, HyperlinkCommandTarget> s_linkTargets;
        private static DTE2 s_dte;
        private static HyperLink s_hyperLink;

        #region IVsTextManagerEvents Members
        public void OnRegisterMarkerType(int iMarkerType)
        {
        }
        public void OnRegisterView(IVsTextView pView)
        {
            s_linkTargets[pView] = new HyperlinkCommandTarget(pView);
            //CompletionSet completionSet = new CompletionSet(null);
            //completionSet.Init(pView, new CommentDeclarations(), true);
        }
        public void OnUnregisterView(IVsTextView pView)
        {
            if (!s_linkTargets.ContainsKey(pView))
                return;

            HyperlinkCommandTarget target = s_linkTargets[pView];
            s_linkTargets.Remove(pView);
            target.Dispose();
        }
        public void OnUserPreferencesChanged(
            VIEWPREFERENCES[] pViewPrefs, FRAMEPREFERENCES[] pFramePrefs,
            LANGPREFERENCES[] pLangPrefs, FONTCOLORPREFERENCES[] pColorPrefs)
        {
        }
        #endregion
    }

    private sealed class ConnectionPointCookie : IDisposable
    {
        #region Member Variables
        private Microsoft.VisualStudio.OLE.Interop.IConnectionPoint connectionPoint;
        private uint cookie;
        private Microsoft.VisualStudio.OLE.Interop.IConnectionPointContainer cpc;
        #endregion

        #region Constructor
        public ConnectionPointCookie(object source, object sink, Type eventInterface)
            : this(source, sink, eventInterface , true)
        {
        }
        public ConnectionPointCookie(object source, object sink, Type eventInterface, bool throwException)
        {
            Exception ex = null;
            if (source is Microsoft.VisualStudio.OLE.Interop.IConnectionPointContainer)
            {
                cpc = (Microsoft.VisualStudio.OLE.Interop.IConnectionPointContainer)source;

                try
                {
                    Guid tmp = eventInterface.GUID;
                    cpc.FindConnectionPoint(ref tmp, out connectionPoint);
                }
                catch
                {
                    connectionPoint = null;
                }

                if (connectionPoint == null)
                {
                    ex = new ArgumentException(/* SR.GetString(SR.ConnectionPoint_SourceIF, eventInterface.Name)*/);
                }
                else if (sink == null || !eventInterface.IsInstanceOfType(sink))
                {
                    ex = new InvalidCastException(/* SR.GetString(SR.ConnectionPoint_SinkIF)*/);
                }
                else
                {
                    try
                    {
                        connectionPoint.Advise(sink, out cookie);
                    }
                    catch
                    {
                        cookie = 0;
                        connectionPoint = null;
                        ex = new Exception(/*SR.GetString(SR.ConnectionPoint_AdviseFailed, eventInterface.Name)*/);
                    }
                }
            }
            else
            {
                ex = new InvalidCastException(/*SR.ConnectionPoint_SourceNotICP)*/);
            }


            if (throwException && (connectionPoint == null || cookie == 0))
            {
                if (ex == null)
                {
                    throw new ArgumentException(/*SR.GetString(SR.ConnectionPoint_CouldNotCreate, eventInterface.Name)*/);
                }
                else
                {
                    throw ex;
                }
            }
        }
        #endregion

        #region Finalizer
        ~ConnectionPointCookie()
        {
#if DEBUG
            //System.Diagnostics.Debug.Assert(connectionPoint == null || cookie == 0,
            //    "We should never finalize an active connection point. (Interface = " +
            //    eventInterface.FullName +
            //    "), allocating code (see stack) is responsible for unhooking the ConnectionPoint by calling Disconnect.  Hookup Stack =\r\n" + callStack);
#endif

            // We must pass false here because chances are that if someone
            // forgot to Dispose this object, the IConnectionPoint is likely to be 
            // a disconnected RCW at this point (for example, we are far along in a 
            // VS shutdown scenario).  The result will be a memory leak, which is the 
            // expected result for an undisposed IDisposable object. [clovett] bug 369592.
            Dispose(false);
        }
        #endregion

        #region Private Members
        private void Dispose(bool disposing)
        {
            if (disposing)
            {
                try
                {
                    if (connectionPoint != null && cookie != 0)
                    {
                        connectionPoint.Unadvise(cookie);
                    }
                }
                finally
                {
                    cookie = 0;
                    connectionPoint = null;
                    cpc = null;
                }
            }
        }
        #endregion

        #region IDisposable Members
        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }
        #endregion

    }
    #endregion
}

/// <summary>
/// Passed to the event handler of code:HyperLink.OnHyperLinkClick. 
/// </summary>
public class HyperLinkEventArgs : EventArgs
{
    // Use to indicate whether normal hyperlink processing should continue. 
    public bool Handled
    {
        get { return handled; }
        set { handled = value; }
    }
    public string Text
    {
        get { return text; }
    }

    #region Private
    private bool handled;
    internal string text;
    #endregion
}



